热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

中点|升序_算法模板链表

篇首语:本文由编程笔记#小编为大家整理,主要介绍了算法模板-----链表相关的知识,希望对你有一定的参考价值。链表基础知识点null异常

篇首语:本文由编程笔记#小编为大家整理,主要介绍了算法模板-----链表相关的知识,希望对你有一定的参考价值。


链表基础知识点


  • null异常的处理
  • dummy node节点
  • 快慢指针
  • 插入一个节点到排序链表
  • 从一个链表中移除一个节点
  • 反转链表
  • 合并两个链表
  • 找到链表的中间节点

通过下面的练习,大部分链表题都能可以应对。
remove-duplicates-from-sorted-list
remove-duplicates-from-sorted-list-ii
reverse-linked-list
reverse-linked-list-ii
merge-two-sorted-lists
partition-list
sort-list
reorder-list
linked-list-cycle
linked-list-cycle-ii
palindrome-linked-list
copy-list-with-random-pointer


常见题型

remove-duplicates-from-sorted-list



给定一个排序链表,删除所有重复的元素,使每个元素只出现一次


// 递归
ListNode* deleteDuplicates(ListNode* head)
if(head == NULL || head->next == NULL) return head;
head->next = deleteDuplicates(head->next);
return head->val == head->next->val ? head->next : head;
// 非递归
ListNode* deleteDuplicates(ListNode* head)
ListNode *cur = head;
while(cur != NULL && cur->next != NULL)
if(cur->val == cur->next->val)
cur->next = cur->next->next; //跳过重复节点
else
cur = cur->next;


return head;

remove-duplicates-from-sorted-list-ii



给定一个排序链表,删除所有重复的节点,只保留原始链表中没有重复出现的。
思路:链表的头节点可能会被删除,需要定义一个dummy node辅助进行删除过程
dummy node 的使用在面试中很常见


ListNode* deleteDuplicates(ListNode* head)
if(head == NULL || head->next == NULL) return head;

ListNode *dummy = new ListNode(-1); //定义dummy node节点进行辅助
dummy->next = head;
head = dummy;

int temp;
while(head->next != NULL && head->next->next != NULL)
if(head->next->val == head->next->next->val)
temp = head->next->val;
while(head->next != NULL && head->next->val == temp)
// 查找所有相同的节点进行删除
head->next = head->next->next;

else
head = head->next;


return dummy->next;


注意点:


  • A->B->C 删除B => A.next = C
    删除用一个dummy node节点进行辅助(允许头节点改变)
    访问X.next、X.value一定要保证X!=NULL(这个很重要,一定要细心)

reverse-linked-list



反转单链表
思路:用一个prev节点保存前向指针,temp保存后向的临时指针。
反转链表的套路要记住,可能在其他题中会使用


// 迭代
ListNode* reverseList(ListNode* head)
ListNode* pre = NULL;
ListNode* cur = head;
while(cur != NULL)
ListNode *later = cur->next;
cur->next = pre;
pre = cur;
cur = later;

return pre;
// 递归
ListNode* reverseList(ListNode* head)
if(head == NULL || head->next == NULL) return head;

ListNode *h = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return h;

reverse-linked-list-ii



反转从位置m到n的链表&#xff0c;使用一趟扫描完成。(1<&#61;m<&#61;n<&#61;链表长度)
思路&#xff1a;先遍历到m处&#xff0c;反转&#xff0c;再拼接后边的。
需要使用dummy node&#xff0c;可能会改变head


ListNode* reverseBetween(ListNode* head, int m, int n)
if(head &#61;&#61; NULL || head->next &#61;&#61; NULL) return head;
ListNode *dummy &#61; new ListNode(-1);
ListNode *pre &#61; dummy;
dummy->next &#61; head;

for(int i &#61; 0; i pre &#61; pre->next;


ListNode *cur &#61; pre->next; // m-1 从此处开始反转
for(int i &#61; m; i ListNode *temp &#61; cur->next;
cur->next &#61; temp->next;
temp->next &#61; pre->next;
pre->next &#61; temp;

return dummy->next;

merge-two-sorted-lists



将两个升序链表合并为新的升序链表&#xff0c;在原链表上进行操作
思路&#xff1a;定义dummy node&#xff0c;链接各个元素


ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
if(l1 &#61;&#61; nullptr) return l2;
if(l2 &#61;&#61; nullptr) return l1;

if(l1->val <&#61; l2->val)
l1->next &#61; mergeTwoLists(l1->next, l2);
return l1;
else
l2->next &#61; mergeTwoLists(l1, l2->next);
return l2;

partition-list



分隔链表&#xff0c;使所有小于x的节点出现在大于或等于x的节点之前。
保留两个分区中每个节点的初始相对位置。
思路&#xff1a;将大于等于x的节点&#xff0c;放到另外一个链表中&#xff0c;最后链接这两个链表


ListNode* partition(ListNode* head, int x)
if(!head) return head;

ListNode *beforeNode &#61; new ListNode(-1);
ListNode *before &#61; beforeNode;
ListNode *afterNode &#61; new ListNode(-1);
ListNode *after &#61; afterNode;

while(head !&#61; NULL)
if(head->val before->next &#61; head;
before &#61; before->next;
else
after->next &#61; head;
after &#61; after->next;

head &#61; head->next;

after->next &#61; NULL;
before->next &#61; afterNode->next;
return beforeNode->next;


当头节点不确定的时候需要使用dummy node


sort-list



在O(nlogn)时间复杂度下&#xff0c;常数级空间复杂度下&#xff0c;对链表进行排序(升序)
思路&#xff1a;使用归并排序&#xff0c;找中点进行合并操作


ListNode* sortList(ListNode* head)
if(head &#61;&#61; NULL || head->next &#61;&#61; NULL) return head;

// 使用快慢指针找到中点
ListNode *slow &#61; head;
ListNode *fast &#61; head->next;
while(fast !&#61; NULL && fast->next !&#61; NULL)
slow &#61; slow->next;
fast &#61; fast->next->next;

ListNode *middleNode &#61; slow->next;
slow->next &#61; NULL;

ListNode *left &#61; sortList(head);
ListNode *right &#61; sortList(middleNode);
return mergeSortList(left, right); // 合并
ListNode* mergeSortList(ListNode* left, ListNode* right)
// 升序合并两个序列
ListNode *dummy &#61; new ListNode(-1);
ListNode *cur &#61; dummy;

while(left !&#61; NULL && right !&#61; NULL)
if(left->val val)
cur->next &#61; left;
left &#61; left->next;
else
cur->next &#61; right;
right &#61; right->next;

cur &#61; cur->next;


while(left !&#61; NULL) //将剩余的left接在后边
cur->next &#61; left;
cur &#61; cur->next;
left &#61; left->next;


while(right !&#61; NULL)
// 将剩余的right接在后边
cur->next &#61; right;
cur &#61; cur->next;
right &#61; right->next;


return dummy->next;


注意&#xff1a;


  • 使用快慢指针的时候需要判断fast 及 fast->next 是否为null
  • 递归mergeSort需要断开中间节点
  • 递归返回条件为head &#61; null 或者 head.next &#61; null

reorder-list



给定一个单链表&#xff0c;L: L0->L1->…->Ln-1->Ln&#xff0c;将其重新排序为&#xff1a;L0->Ln->L1->Ln-1…
不能只是单纯改变节点的值&#xff0c;需要进行节点变换
思路&#xff0c;找到中间节点&#xff0c;反转后边部分&#xff0c;然后合并前后两个链表


void reorderList(ListNode* head)
if(head &#61;&#61; nullptr) return;

// 快慢指针找到中点
ListNode *slow &#61; head;
ListNode *fast &#61; head->next;
while(fast !&#61; nullptr && fast->next !&#61; nullptr)
slow &#61; slow->next;
fast &#61; fast->next->next;


ListNode *middleNode &#61; slow->next;
ListNode *right &#61; reverseList(slow->next); //反转后边的链表
slow->next &#61; nullptr;
head &#61; mergeTwoList(head, right);
ListNode *mergeTwoList(ListNode *left, ListNode *right)
// 合并两个链表
ListNode *dummy &#61; new ListNode(-1);
ListNode *cur &#61; dummy;
bool flag &#61; true; // true: left, false: right
while(left !&#61; nullptr && right !&#61; nullptr)
if(flag)
cur->next &#61; left;
left &#61; left->next;
else
cur->next &#61; right;
right &#61; right->next;

flag &#61; !flag;
cur &#61; cur->next;

while(left !&#61; nullptr)
// 拼接left剩余部分
cur->next &#61; left;
cur &#61; cur->next;
left &#61; left->next;

while(right !&#61; nullptr)
//拼接right剩余部分
cur->next &#61; right;
cur &#61; cur->next;
right &#61; right->next;

return dummy->next;
ListNode *reverseList(ListNode *root)
//反转链表
ListNode *pre &#61; nullptr;
ListNode *cur &#61; root;
while(cur !&#61; nullptr)
ListNode *temp &#61; cur->next;
cur->next &#61; pre;
pre &#61; cur;
cur &#61; temp;

return pre;

linked-list-cycle



给定一个链表&#xff0c;判断是否有环
思路&#xff1a;快慢指针&#xff0c;快慢指针相同&#xff0c;则有环。
如果有环&#xff0c;在环内每走一步快慢指针距离会减一


bool hasCycle(ListNode* head)
if(head &#61;&#61; NULL) return false;

ListNode *slow &#61; head;
ListNode *fast &#61; head->next;
while(fast !&#61; NULL && fast->next !&#61; NULL)
if(fast &#61;&#61; slow) return true;
fast &#61; fast->next->next;
slow &#61; slow->next;

return false;

linked-list-cycle-ii



给定一个链表&#xff0c; 判断入环的第一个节点&#xff0c;没有环则返回-1。
证明&#xff1a;
假设链表中环外部分的长度为a&#xff0c;slow进入环内又走了b的距离与fast相遇&#xff0c;此时fast已经走完了环的n圈&#xff0c;因此fast走过的距离为a&#43;n(b&#43;c)&#43;b&#61;a&#43;(n&#43;1)b&#43;nc。
根据题意&#xff0c;fast走过的距离使slow走过距离的2倍。则&#xff1a;
a&#43;(n&#43;1)b&#43;nc&#61;2(a&#43;b) &#61;> a &#61; c&#43;(n-1)(b&#43;c)。
所以当slow与fast相遇时&#xff0c;我们再额外使用一个指针ptr&#xff0c;指向链表头部&#xff0c;随后它与slow每次向后移动一个位置。最终会在入环点相遇。


ListNode* detectCycle(ListNode* head)
if(head &#61;&#61; NULL) return head;

ListNode *fast &#61; head->next;
ListNode *slow &#61; head;
while(fast !&#61; NULL && fast->next !&#61; NULL)
if(fast &#61;&#61; slow)
//第一次相遇&#xff0c;判断有环
//调整slow和fast的步长&#xff0c;等待第二次相遇
slow &#61; head;
fast &#61; fast->next;
while(fast !&#61; slow)
//第二次相遇 入环口
fast &#61; fast->next;
slow &#61; slow->next;

return slow;

fast &#61; fast->next->next;
slow &#61; slow->next;

return NULL;


注意&#xff1a;


  • 指针比较时直接比较指针&#xff0c;不要用值进行比较&#xff0c;链表中可能存在重复值
  • 第一次相遇后&#xff0c;快指针需要从下一个节点开始和头指针一起匀速运动

另一种方式是fast&#61;head, slow&#61;head。

ListNode *detectCycle(ListNode *head)
if(head &#61;&#61; NULL) return head;

ListNode *fast &#61; head;
ListNode *slow &#61; head;
while(fast !&#61; NULL && fast->next !&#61; NULL)
fast &#61; fast->next->next;
slow &#61; slow->next;
if(fast &#61;&#61; slow)
fast &#61; head;
while(fast !&#61; slow)
//第二次相遇就是环的入口
fast &#61; fast->next;
slow &#61; slow->next;

return slow;


return NULL;


两个不同点&#xff1a;


  • fast初始化为head.next 则中点在slow.next
  • fast初始化为head&#xff0c;则中点在slow。
    一般使用fast&#61;head.next较多&#xff0c;这样可以知道中点的上一个节点&#xff0c;可以进行删除操作。

palindrome-linked-list



判断一个链表是否是回文链表
找到中点&#xff0c;反转后边的链表&#xff0c;前后链表进行比较


bool isPalindrome(ListNode* head)
if(head &#61;&#61; NULL) return true;

// 快慢指针找到中点
// 中点是slow->next
ListNode *slow &#61; head, *fast &#61; head->next;
while(fast !&#61; NULL && fast->next !&#61; NULL)
slow &#61; slow->next;
fast &#61; fast->next->next;


ListNode *tail &#61; reverseList(slow->next);
// 断开两个链表
slow->next &#61; NULL;
while(head !&#61; NULL && tail !&#61; NULL)
if(head->val !&#61; tail->val) return false;
head &#61; head->next;
tail &#61; tail->next;

return true;
ListNode *reverseList(ListNode *head)
// 反转链表
if(head &#61;&#61; NULL) return head;
ListNode *pre &#61; NULL;
while(head !&#61; NULL)
ListNode *temp &#61; head->next;
head->next &#61; pre;
pre &#61; head;
head &#61; temp;

return pre;

copy-list-with-random-pointer



给定一个链表&#xff0c;每个节点包含一个额外增加的随机指针&#xff0c;该指针指向链表中的任意节点或空节点&#xff0c;返回这个链表的深拷贝。
思路&#xff1a;用hash表存储指针&#xff0c;复制节点跟在原节点的后边。


Node *copyRandomList(Node* head)
if(head &#61;&#61; NULL) return head;

// 复制节点跟在原节点后边
// 1->1&#39;->2->2&#39;...
Node *cur &#61; head;
while(cur !&#61; NULL)
Node *clone &#61; new Node(cur->val);
clone->next &#61; cur->next;
cur->next &#61; clone;
cur &#61; clone->next;


//处理随机指针
cur &#61; head;
while(cur !&#61; NULL)
cur->next->random &#61; (cur->random !&#61; NULL) ? cur->random->next : NULL;
cur &#61; cur->next->next;


// 分离两个链表
cur &#61; head;
Node *cloneRandom &#61; cur->next;
while(cur !&#61; NULL && cur->next !&#61; NULL)
Node *temp &#61; cur->next;
cur->next &#61; cur->next->next;
cur &#61; temp;

//原始链表头 head
//克隆链表头 cloneRandom
return cloneRandom;

推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • 提升Python编程效率的十点建议
    本文介绍了提升Python编程效率的十点建议,包括不使用分号、选择合适的代码编辑器、遵循Python代码规范等。这些建议可以帮助开发者节省时间,提高编程效率。同时,还提供了相关参考链接供读者深入学习。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
author-avatar
泡乙唐
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有